
package at.fhj.ase.crypto.server.net;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;

import at.fhj.ase.crypto.common.util.LogUtil;
import at.fhj.ase.crypto.common.util.NetworkUtil;

/**
 * Class responsible for dealing with arriving client connections.<br>
 * If one client arrives,
 * <ul>
 * <li>a socket to it is created (= a connection is established)<br>
 * <li>a {@link ConnectionHandler} is created<br>
 * </ul>
 * We could also say: For each connecting client, we have/create one connection handler (1:1 realtionship).
 * <p>
 * After a client was successfully connected, it is added to the pool of known/connected clients.<br>
 * This is necessary as we want to broadcast messages to all the clients.
 * 
 * @author sfe | www.fh-joanneum.at | Practical Aspects of Cryptography
 */
public class ClientManager implements Runnable {
  /** The sole singleton instance of this client manager */
  private static ClientManager INSTANCE;

  /** The logging instance */
  private final Logger logger = LogUtil.getLogger(this);

  /** The one and only server socket */
  private final ServerSocket serverSocket;

  /** All the client connections that have been connected to this server */
  private final List<ServerClientConnection> connections = new ArrayList<ServerClientConnection>();

  /**
   * Sole access method of class {@link ClientManager} (Singleton implementation)
   * 
   * @param port number the port we want to open the {@link ServerSocket} (constructor)
   * @return the sole instance of this class
   * @throws IOException if the socket could not be opened (thrown by constructor)
   */
  public static ClientManager getInstance(final int port) throws IOException {
    if (INSTANCE == null) {
      INSTANCE = new ClientManager(port);
    }
    return INSTANCE;
  }

  /**
   * Starts this client manager by initiating a server socket
   * 
   * @param port the port number the {@link ServerSocket} is started with (checked for validity)
   * @throws IOException if the {@link ServerSocket} could not be instantiated
   */
  private ClientManager(final int port) throws IOException {
    NetworkUtil.checkPortNumber(port);

    logger.debug("Trying to create server socket...");
    this.serverSocket = new ServerSocket(port);
    logger.info("Server up and running on port " + port);
  }

  /**
   * Accepts clients connections as long as this thread is interrupted by querying {@link Thread#isInterrupted()}.<br>
   * If there is a problem at accepting the client connection, the exception is logged at the log instance.
   */
  @Override
  public void run() {
    while (!Thread.currentThread().isInterrupted()) {
      try {
        acceptClientConnection(serverSocket.accept());
      } catch (IOException e) {
        logger.error("Could not accept client connection", e);
      }
    }
  }

  /**
   * Method performing the actual work when a client connection arrives.
   * <p>
   * It gives each arriving client connection/socket an own {@link ConnectionHandler}.<br>
   * For convenience reasons, an {@link ServerClientConnection} is created internally.<br>
   * This convenience connection is also saved into the pool of client connection the server has to know for broadcasting messages.
   * 
   * @param acceptedClientSocket the client socket that has been accepted (also client connection)
   * @throws IOException if there happened an error while instantiating the {@link ConnectionHandler} for this client connection
   */
  private void acceptClientConnection(Socket acceptedClientSocket) throws IOException {
    logger.debug("Client connection arrived, creating seperate ConnectionHandler (thread) for that client...");

    final ConnectionHandler connectionHandler = new ConnectionHandler(acceptedClientSocket, this);
    addConnection(connectionHandler.getServerClientConnection());
  }

  /**
   * Removes a {@link ServerClientConnection} from the list of client connections (pool) and interrupts its Thread by calling {@link Thread#interrupt()}
   * 
   * @param clientConnection the server-client-connection from the server to one client that shall be removed from the pool
   */
  public void removeConnection(ServerClientConnection clientConnection) {
    logger.debug("Removing connection " + clientConnection);
    connections.remove(clientConnection);
  }

  /**
   * Adds a {@link ServerClientConnection} to the list of available client connections (pool)
   * 
   * @param clientConnection the client to be added to the available client connections (pool)
   */
  public void addConnection(ServerClientConnection clientConnection) {
    logger.debug("Adding connection " + clientConnection);
    connections.add(clientConnection);
  }

  /**
   * Sends a broadcast to all registered client connections in the pool with the provided <code>message</code>,<br>
   * but not to the provided <code>srvClntConnSource</code> as this is the source connection we want to broadcast from.
   * 
   * @param message the message as a {@link String} that shall be broadcasted to all other clients
   * @param srvClntConnSource the client connection that originally sent the message. We don't want to send the message back to this connection again
   */
  void broadcastToAllClients(final String message, ServerClientConnection srvClntConnSource) {
    for (ServerClientConnection scc : connections) {
      // do not again send it to the client that sent the message but send the original message!
      if (scc != srvClntConnSource) {
        scc.getSocketWrapper().sendMessage(message);
      }
    }
  }

  /** Starts this thread ({@link Runnable}) awaiting client connections */
  public void start() {
    logger.debug("Trying to start thread...");
    new Thread(this).start();
    logger.debug("Thread successfully started, waiting to accept client connections...");
  }
}
